﻿using System;
using System.Collections.Generic;
using System.Linq;
using RayDen.Library.Components.HashGrids;
using RayDen.Library.Components.SystemComponents;
using RayDen.Library.Core.Primitives;
using RayDen.Library.Core;

namespace RayDen.RayEngine.Engines.ProgressivePhotonMapping {

    public interface IEyePointHashGrid
    {
        void ReHash();
        void StoreFlux(float Alpha, ref Point hitPoint, ref Normal normal, Vector wo, ref RgbSpectrum throughtPut);

        List<PPMHitPoint> GetHitPoints();
    }


    public class Grid : GenericHashGrid<PPMHitPoint> , IEyePointHashGrid
    {
        public Grid(int max, IEnumerable<PPMHitPoint> points) : base(max)
        {
            MaxPhotonsPerCell = 1;
            this.hitPoints = points.Select(item => new HashGridElement<PPMHitPoint>
            {
                Data = item,
                Position = item.position
            }).ToList();
        }


        #region IHashGrid Members


        public void StoreFlux( float alpha, ref Point hitPoint, ref Normal shadeN, Vector wi, ref RgbSpectrum throughtPut )
        {
            // Look for eye path hit points near the current hit point
            Vector hh = (hitPoint - hpbbox.Min) * invCellSize;
            int ix = Math.Abs((int)(hh.x));
            int iy = Math.Abs((int)(hh.y));
            int iz = Math.Abs((int)(hh.z));

            var hps = hashGrid[Hash(ix, iy, iz)];
            if (hps != null)
            {
                for (int i = 0; i < hps.Length; i++)
                {
                    Vector v;
                    Point pp = hitPoints[hps[i]].Position;
                    Point.Sub(ref pp, ref hitPoint, out v);
                    // TODO: use configurable parameter for normal treshold
                    float dist2 = NormalThreshold;
                    //v.Length2();
                    if ((dist2 > hitPoints[hps[i]].Data.photonRadius2))
                        continue;

                    float dot = Vector.Dot(ref hitPoints[hps[i]].Data.normal, ref wi);
                    if (dot <= NormalThreshold)
                        continue;

                    if ((Normal.Dot(ref hitPoints[hps[i]].Data.normal, ref shadeN) > NormalThreshold) && (Vector.Dot(ref v, ref v) <= hitPoints[hps[i]].Data.photonRadius2)) 
                    {
                        // succ++;
                        float g = (hitPoints[hps[i]].Data.accumPhotonCount * alpha + alpha) / (hitPoints[hps[i]].Data.accumPhotonCount * alpha + 1f);
                        hitPoints[hps[i]].Data.photonRadius2 *= g;
                        hitPoints[hps[i]].Data.accumPhotonCount++;

                        RgbSpectrum fc;
                        var Throughput = RgbSpectrum.Unit;
                        hitPoints[hps[i]].Data.material.f(ref hitPoints[hps[i]].Data.wo, ref wi, ref shadeN, ref Throughput, out fc);
                        RgbSpectrum flux = throughtPut *fc* Vector.AbsDot(ref shadeN, ref wi) * hitPoints[hps[i]].Data.throughput;
                        hitPoints[hps[i]].Data.accumReflectedFlux = (hitPoints[hps[i]].Data.accumReflectedFlux + flux) * g;
                    }
                }
            }
        }

        public List<PPMHitPoint> GetHitPoints()
        {
            return this.GetPlainData().ToList();
        }

        #endregion
    }

    [Obsolete]
     class HashGrid  : IEyePointHashGrid
    {
        public static float NormalThreshold = 0.001f;
        public static int MaxPhotonsPerCell = 4;
        public List<PPMHitPoint> hitPoints;


        internal long NonZeroHitpoints
        {
            get { return this.hitPoints.Count(i => i.accumPhotonCount > 0); }
        }

        uint hashGridSize;
        float invCellSize;
        List<PPMHitPoint>[] hashGrid;
        public AABB hpbbox;
        private int width;
        private int height;
        public HashGrid(List<PPMHitPoint> hps, int w, int h) {
            this.width = w;
            this.height = h;
            hitPoints = hps;
            hashGrid = null;

            ReHash();
        }

        public void ReHash() {
            Tracer.TraceLine("Rehashing...");
            hitPoints = hitPoints.Where(item => item != null).ToList();
            var hitPointsCount = hitPoints.Count;
            hpbbox = new AABB();
            hpbbox.Reset();
            for (int n = 0; n < hitPoints.Count; n++) {
                hpbbox.fit(hitPoints[n].position);
            }
            Vector ssize = hpbbox.Size;
            float photonRadius1 = ((ssize.x + ssize.y + ssize.z) / 3000f);// / ((width + height) / 2f) * 2f;

            // Expand the bounding box by used radius
            hpbbox.Expand(photonRadius1);

            // Initialize hit points field
            float photonRadius2 = photonRadius1 * photonRadius1;
            for (int i = 0; i < (hitPoints).Count; ++i) {
                var hp = hitPoints[i];

                if (hp.type == HitPointType.Surface) {
                    hp.photonRadius2 = photonRadius2;
                    hp.accumPhotonCount = 0;
                    hp.accumReflectedFlux = new RgbSpectrum();
                }
            }

            // Calculate the size of the grid cell
            float maxPhotonRadius2 = 0f;
            for (int i = 0; i < hitPointsCount; ++i) {
                var hp = hitPoints[i];

                if (hp.type == HitPointType.Surface)
                    maxPhotonRadius2 = Math.Max(maxPhotonRadius2, hp.photonRadius2);
            }

            float cellSize = MathLab.Sqrt(maxPhotonRadius2) * 2f;
            //std::cerr << "Hash grid cell size: " << cellSize <<std::endl;
            invCellSize = 1f / cellSize;
            if (float.IsInfinity(invCellSize))
                throw new ArgumentException("Cell size is invalid");

            // TODO: add a tunable parameter for hashgrid size
            hashGridSize = (uint)hitPointsCount;
            if (hashGrid == null) {
                hashGrid = new List<PPMHitPoint>[hashGridSize];

                for (var i = 0; i < hashGridSize; ++i)
                    hashGrid[i] = null;
            }
            else {
                for (var i = 0; i < hashGridSize; ++i) {
                    hashGrid[i] = null;
                }
            }

            //std::cerr << "Building hit points hash grid:" << std::endl;
            //std::cerr << "  0k/" << hitPointsCount / 1000 << "k" <<std::endl;
            //unsigned int maxPathCount = 0;
            //double lastPrintTime = WallClockTime();
            ulong entryCount = 0;
            for (var i = 0; i < hitPointsCount; ++i) {
                //if (WallClockTime() - lastPrintTime > 2.0) {
                //    std::cerr << "  " << i / 1000 << "k/" << hitPointsCount / 1000 << "k" <<std::endl;
                //    lastPrintTime = WallClockTime();
                //}

                var hp = hitPoints[i];
                //var hpl = 

                if (hp.type == HitPointType.Surface) {
                    float photonRadius = MathLab.Sqrt(hp.photonRadius2);
                    Vector rad = new Vector(photonRadius, photonRadius, photonRadius);
                    var bMin = ((hp.position - rad) - (Vector) hpbbox.Min) * invCellSize;
                    var bMax = ((hp.position + rad) - (Vector)hpbbox.Min) * invCellSize;

                    for (int iz = Math.Abs((int)bMin.z); iz <= Math.Abs((int)bMax.z); iz++) {
                        for (int iy = Math.Abs((int)bMin.y); iy <= Math.Abs((int)bMax.y); iy++) {
                            for (int ix = Math.Abs((int)bMin.x); ix <= Math.Abs((int)bMax.x); ix++) {
                                int hv = (int)Hash(ix, iy, iz);

                                if (hashGrid[hv] == null)
                                    hashGrid[hv] = new List<PPMHitPoint>(MaxPhotonsPerCell);;

                                if (hashGrid[hv].Count > MaxPhotonsPerCell)
                                    continue;

                                hashGrid[hv].Add(hp);

                                ++entryCount;

                                /*// hashGrid[hv].size() is very slow to execute
                                if (hashGrid[hv].size() > maxPathCount)
                                    maxPathCount = hashGrid[hv].size();*/
                            }
                        }
                    }
                }
            }
            //std::cerr << "Max. hit points in a single hash grid entry: " << maxPathCount << std::endl;
            //std::cerr << "Total hash grid entry: " << entryCount << std::endl;
            //std::cerr << "Avg. hit points in a single hash grid entry: " << entryCount / hashGridSize << std::endl;

            // HashGrid debug code
            /*for (unsigned int i = 0; i < hashGridSize; ++i) {
                if (hashGrid[i]) {
                    if (hashGrid[i].size() > 10) {
                        std::cerr << "HashGrid[" << i << "].size() = " <<hashGrid[i].size() << std::endl;
                    }
                }
            }*/
            Tracer.TraceLine("Complete...");
        }


        public void AddFlux(float alpha, ref Point hitPoint, ref Normal shadeN, ref Vector wi, ref RgbSpectrum photonFlux) {
            // Look for eye path hit points near the current hit point
            Vector hh = (hitPoint - hpbbox.Min) * invCellSize;
            int ix = Math.Abs((int)(hh.x));
            int iy = Math.Abs((int)(hh.y));
            int iz = Math.Abs((int)(hh.z));

            var hps = hashGrid[Hash(ix, iy, iz)];
            if (hps != null) {
                for (int i = 0; i < hps.Count; i++) {
                    var hp = hps[i];
                    Vector v;
                    Point.Sub(ref hp.position, ref hitPoint, out v);
                    // TODO: use configurable parameter for normal treshold
                    float dist2 =
                        //NormalThreshold;
                        v.Length2();
                    if ((dist2 > hp.photonRadius2))
                        continue;

                    float dot = Vector.Dot(ref hp.normal, ref wi);
                    if (dot <= NormalThreshold)
                        continue;

                    if( (Normal.Dot(ref hp.normal, ref shadeN) > NormalThreshold) && (Vector.Dot(ref v, ref v) <= hp.photonRadius2) ) 
                    {
                        // succ++;
                        float g = (hp.accumPhotonCount * alpha + alpha) / (hp.accumPhotonCount * alpha + 1f);
                        hp.photonRadius2 *= g;
                        hp.accumPhotonCount++;

                        RgbSpectrum fc;
                        var Throughput = RgbSpectrum.Unit;

                        hp.material.f(ref hp.wo, ref wi, ref shadeN, ref Throughput, out fc);
                        RgbSpectrum flux = photonFlux * fc * dot * hp.throughput;
                        hp.accumReflectedFlux += (flux) * g;
                    }
                    //else {
                    //    fail++;
                    //}
                }


            }


        }


        public void AddFlux(float alpha, ref Point hitPoint, ref Normal shadeN, Vector wi, float photonFlux, int nSpectra) {
            // Look for eye path hit points near the current hit point
            Vector hh = (hitPoint - hpbbox.Min) * invCellSize;
            int ix = Math.Abs((int)(hh.x));
            int iy = Math.Abs((int)(hh.y));
            int iz = Math.Abs((int)(hh.z));

            var hps = hashGrid[Hash(ix, iy, iz)];
            if (hps != null) {
                for (int i = 0; i < hps.Count; i++) {
                    var hp = hps[i];
                    Vector v;
                    Point.Sub(ref hp.position, ref hitPoint, out v);
                    // TODO: use configurable parameter for normal treshold
                    float dist2 =
                        //NormalThreshold;
                        v.Length2();
                    if ((dist2 > hp.photonRadius2))
                        continue;

                    float dot = Vector.Dot(ref hp.normal, ref wi);
                    if (dot <= NormalThreshold)
                        continue;

                    if ((Normal.Dot(ref hp.normal, ref shadeN) > NormalThreshold) && (Vector.Dot(ref v, ref v) <= hp.photonRadius2)) 
                    {
                        // succ++;
                        float g = (hp.accumPhotonCount * alpha + alpha) / (hp.accumPhotonCount * alpha + 1f);
                        hp.photonRadius2 *= g;
                        hp.accumPhotonCount++;

                        RgbSpectrum fc;
                        var Throughput = RgbSpectrum.Unit;

                        hp.material.f(ref hp.wo, ref wi, ref shadeN, ref Throughput, out fc);
                        var flux = photonFlux * fc* Vector.AbsDot(ref shadeN, ref wi) * hp.throughput;
                        hp.accumReflectedFlux = (hp.accumReflectedFlux + flux) * g;
                    }
                    //else {
                    //    fail++;
                    //}
                }


            }


        }


        public void StoreFlux(float alpha, ref Point hitPoint, ref Normal shadeN, Vector wi, ref RgbSpectrum photonFlux) {
            // Look for eye path hit points near the current hit point
            Vector hh = (hitPoint - hpbbox.Min) * invCellSize;
            int ix = Math.Abs((int)(hh.x));
            int iy = Math.Abs((int)(hh.y));
            int iz = Math.Abs((int)(hh.z));

            var hps = hashGrid[Hash(ix, iy, iz)];
            if (hps != null) {
                for (int i = 0; i < hps.Count; i++)
                {
                    Vector v;
                    Point.Sub(ref  hps[i].position, ref hitPoint, out v);
                    // TODO: use configurable parameter for normal treshold
                    float dist2 = NormalThreshold;
                    //v.Length2();
                    if ((dist2 > hps[i].photonRadius2))
                        continue;

                    float dot = Vector.Dot(ref  hps[i].normal, ref wi);
                   if (dot <= NormalThreshold) continue;

                   //if ((Normal.Dot(ref hps[i].normal, ref shadeN) > NormalThreshold) && (Vector.Dot(ref v, ref v) <= hps[i].photonRadius2)) 
                    {
                        // succ++;
                        float g = (hps[i].accumPhotonCount * alpha + alpha) / (hps[i].accumPhotonCount * alpha + 1f);
                        hps[i].photonRadius2 *= g;
                        hps[i].accumPhotonCount++;

                        RgbSpectrum fc;
                        var Throughput = RgbSpectrum.Unit;
                        hps[i].material.f(ref hps[i].wo, ref wi, ref shadeN, ref Throughput, out fc);
                        RgbSpectrum flux = photonFlux *fc* Vector.AbsDot(ref shadeN, ref wi) * hps[i].throughput;
                        hps[i].accumReflectedFlux = (hps[i].accumReflectedFlux + flux) * g;
                    }
                }
            }
        }

        public bool IsVisible(ref Point hitPoint) {
            Vector hh = (hitPoint - hpbbox.Min) * invCellSize;
            int ix = Math.Abs((int)(hh.x));
            int iy = Math.Abs((int)(hh.y));
            int iz = Math.Abs((int)(hh.z));

            var hps = hashGrid[Hash(ix, iy, iz)];
            return hps != null && hps.Any(item => item.type == HitPointType.Surface);
        }
        private uint Hash(int ix, int iy, int iz) {
            return ((uint)((ix * 73856093U) ^ (iy * 19349663U) ^ (iz * 83492791U)) % hashGridSize);
        }


        public List<PPMHitPoint> GetHitPoints()
        {
            return this.hitPoints;
        }
    }
}